Skip to content

Ask segment whether a post-comma run is name text (#319) - #327

Merged
derek73 merged 11 commits into
masterfrom
fix/wholly-suffix-predicate
Aug 3, 2026
Merged

Ask segment whether a post-comma run is name text (#319)#327
derek73 merged 11 commits into
masterfrom
fix/wholly-suffix-predicate

Conversation

@derek73

@derek73 derek73 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #319.

The bug

Same credential, three spellings, two answers:

parse("田中さん, PhD")     # title PhD, family 田中, suffix さん      <- peels
parse("田中さん, V.")      # family '田中さん', given 'V.'            <- does not
parse("田中さん, Ph. D.")  # family '田中さん', suffix 'Ph. D.'       <- does not

#312 scoped the peel's site to segments[:2] under a family comma, on the premise that segments[1] is name text. segment() does not guarantee it: SUFFIX_COMMA needs suffixy(groups[1]) and len(groups[0]) > 1, so a one-word part before the comma falls through to FAMILY_COMMA even when the part after it is entirely suffix-shaped. The peel walked into a run of post-nominals, took V. for its site, found no listed tail there and abandoned — silently.

The fix

Stop re-deriving suffix-ness token by token and ask segment()'s own question. suffixy is lifted into _vocab.is_wholly_suffix(texts, lexicon, policy) so the two stages cannot drift, and the peel declines a wholly-suffix second run.

With a second condition, which is not decoration. Every honorific tail is also a suffix word (Lexicon enforces it), so a glued honorific is itself part of what makes its run read as suffix-shaped — the predicate is circular at this call site. segments[0] must therefore offer a peel site of its own before the second run is declined, established by a _peel_site helper the gate and the peel both call, so the gate asks the peel's own question rather than an approximation of it.

Without that condition 이, J.씨 loses its given name entirely. See below.

Note on the name

is_wholly_suffix is not the plural of _is_post_nominal. That one asks is_suffix_strict of a single token; this asks the policy-selected predicate of a run, plus period_joined_vocab, delimiter transparency and the Ph./D. merge. V. is a suffix run and is not a post-nominal — and confusing the two is the #319 defect. is_wholly_suffix echoes the house whole=True idiom, which already carries the non-empty rule; all_suffixes would have promised the opposite, since Python's all([]) is True.

What the review round changed

Four reviewers went over the branch after the first six commits. The last four commits are the result, and the first of them fixes a regression the original change introduced.

838de67 — a name lost its given name. With the decline unconditional, period_joined_vocab counted a glued honorific as evidence its run was suffix-shaped:

이, J.씨   before: family '이', suffix 'J.씨'          <- given empty
           after:  given 'J.', family '이', suffix '씨'

265 lost peels across policies, 104 under default, reachable with no configuration and visible through HumanName. lenient_comma_suffixes=False did not gate it. Fixed by the site condition above.

a795c49 and a3ae04f — six mutation survivors. A wholesale revert of the fix originally killed only two case rows; the eight-test peel section in test_script_segment.py was untouched by it. The decline gate had three independent unpinned axes — run length, segment index, segment count — plus extra_suffix_delimiters reaching the peel (documented, untested) and the Ph./D. merge past position 0 (which silently flips a Latin input's structure). All now pinned at the stage layer, each verified to fail its mutant.

60f1922, 346b6d9, 1226ffd — prose. Sixteen findings, in four distinct staleness shapes: claims about a traversal that changed; a contract in a module header; a mechanism attributed to the wrong cause; and exhaustiveness claims (exactly, whenever, declined) stated more strongly than they hold.

Verification

  • Suite 2936, mypy and ruff clean, Sphinx -W clean, doctests clean
  • Differential: 748 names, unexplained: 0
  • The extraction proven faithful over 339,660 (input, policy) pairs: every one equals either the pre-fix tree (gate silent) or the pre-Glued honorific: "田中さん, V." does not peel where "田中さん, PhD" does #319 tree (gate fired) — zero novel behaviors
  • Lost-peel class closed, verified independently at 84,915 inputs x 4 policies with a negative control first: the detector reports 3,638 losses against the pre-gate commit before reporting 0 against this one
  • zh and ko equivalents pinned alongside the Japanese ones; the fix is script-agnostic

Known, not addressed

expected_changes.toml promises that trailing Ph. D. healing is parity and that "if it ever starts diffing, the harness must fail" — it does not. fix(comma-family)'s name_regex is just ",", so a suffix-only diff on a comma-bearing name is absorbed. The divergence (1.4.0 "Ph. D., Jr." vs 2.x "Jr. Ph. D.") is now pinned by a case row instead. Narrowing the rule would need classify() to have a real specificity order — today it returns the first match across two sort tiers, so a narrower rule could only win by file position, reintroducing the load-bearing file order that sort exists to prevent. That is a harness change and wants its own decision.

🤖 Generated with Claude Code

derek73 and others added 10 commits August 2, 2026 13:34
Pure refactor -- behavior unchanged. The peel needs the same question
segment already answers ("is this run name text?"), and a copy in
_script_segment would be a third definition of one rule.

Named for the house idiom rather than the obvious alternatives:
_script_matcher's whole=True already means "every element, and empty
does not count", which is exactly this predicate's empty rule.
all_suffixes would have promised the opposite, since Python's all([])
is True. is_suffix_run reads as a third sibling of is_suffix_strict /
is_suffix_lenient, which are the same question at token scale -- and
confusing those IS #319.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The peel scoped itself to segments[:2] under a family comma on the
premise that segments[1] is name text. segment does not guarantee it:
a one-word part before the comma reads as FAMILY_COMMA even when the
part after it is entirely suffix-shaped, so the scan walked into a run
of post-nominals, took an initial-shaped token as the site, found no
tail there and gave up.

Ask segment's own predicate instead of re-deriving it. '田中さん, V.'
and '田中さん, Ph. D.' now peel as '田中さん, PhD' already did -- that
spelling peeled under the old scope because 'PhD' satisfies
_is_post_nominal's strict test, so the scan-back stepped OVER it and
reached 田中さん; 'V.' and the 'Ph.'/'D.' pair do not, which is why
one credential in three spellings gave two answers. One answer now.

Policy(lenient_comma_suffixes=False) keeps the old behavior on purpose:
the knob makes the strict predicate read 'V.' as name text, so the run
is scanned and the peel is abandoned as before. That is the knob doing
its job. The case rows pinning both sides come with the next commit --
this one leaves the #318 known-limit row failing on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ja_honorific_glued_family_comma_suffixy_second_run was a known limit
stated rather than fixed, on the grounds that closing it wanted
segment's suffixy test extracted into a shared predicate. That
predicate is _vocab.is_wholly_suffix and the peel now asks it, so the
row moves: '田中さん, V.' gives family 田中 / suffix さん, and the
classification goes parity -> fix(#319).

Two rows come with it, both UNDETERMINED until they are measured
against 1.4.0. The credential-pair row is the third spelling #319
named and the only one the Ph./D. merge reaches, which is what folds
its run into a single wholly-suffix unit. The strict-knob row is the
same input as the flipped one under
Policy(lenient_comma_suffixes=False), pinning the pre-#319 fields
where they remain reachable.

The notes are careful about two things the rows would otherwise
overclaim. The peel now gives one answer across all three spellings,
but the DOWNSTREAM placement still differs by spelling -- title 'PhD',
given 'V.', suffix 'さん, Ph. D.' -- and that is assign's question. And
the strict knob does not blanket-freeze the old behavior: it holds for
the initial-shaped suffix words, where the strict/lenient gap lives,
but not for 'Ph. D.', whose merged form satisfies is_suffix_strict
too, so that run is declined and the peel fires under the knob as
well.

Four neighbouring notes made claims this falsifies. The property that
moved is WHICH RUNS THE PEEL SCANS: every family-comma row whose
post-comma run is wholly suffix now scans segments[0] alone, so any
note describing the scan crossing the comma is stale whether or not
its row still passes.

  - ja_honorific_glued_family_comma said the scan steps OVER PhD to
    reach 田中さん and that the spaced form peels for the same reason.
    The run is declined instead and PhD is never examined; the two
    spellings now agree by different mechanisms.
  - ja_honorific_with_a_period_no_comma justified its existence by the
    comma form flattening TWO runs where it has one. The comma form
    flattens one now. Its real distinction is that this is the row
    where #320's fix acts through the scan-back.
  - ja_honorific_period_does_not_stop_the_peel said both runs were
    always in the peel's reach and only the strict test's answer
    moved. #319 took the second run back out of reach, and the veto
    now acts through is_wholly_suffix rather than the scan-back.
    Simulating the pre-#320 veto still strands さん, so the fields and
    the classification stand while the mechanism prose is rewritten.
  - ko_honorific_period_under_strict_comma_suffixes called itself the
    table's only exercise of lenient_comma_suffixes; it is one of two.

ko_honorific_glued_given_suffix_comma_initial also claimed to be the
only row noticing a scan widened to every segment. Checked by patching
the scope out: the flipped row and credential_pair notice as well,
from the family-comma side. The strict-knob row does NOT -- under the
knob is_wholly_suffix is False on that run, so the scan already
crossed and widening changes nothing. It remains the only suffix comma
of the three.

The module docstring gains UNDETERMINED as an explicitly temporary
fourth classification.

corpus_cjk.jsonl is generated from the case table and gains the one
new text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both #319 rows added in the previous commit carried the placeholder
classification because nobody had run their inputs against 1.4.0 yet.
Measured, with the worker's pin actually reporting 1.4.0 from inside
the interpreter that answered:

  '田中さん, Ph. D.'  ->  first 田中さん, suffix 'Ph. D.'
  '田中さん, V.'      ->  first 'V.', last 田中さん

ja_honorific_glued_family_comma_credential_pair is fix(#319). It
carries two deviations from 1.4.0, as ja_honorific_glued_family_comma
does: first -> family is comma-family's and 2.0 already had it before
this branch (family 田中さん / suffix 'Ph. D.', measured at the commit
before the peel change), while the peel that takes さん off 田中さん is
what #319 moves.

ja_honorific_glued_family_comma_strict_knob is parity. The knob has no
v1 spelling, so as with ko_honorific_period_under_strict_comma_suffixes
the row is judged against 1.4.0's single reading of the same text --
and there it agrees field for field, which is precisely what the knob
exists to keep reachable.

The differential harness is clean at these commits: 743 names, 99
intentional diffs, unexplained 0, no new rule needed in
expected_changes.toml. Both #319 inputs land under the existing
fix(cjk-comma-compound) rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three prose sites that stated the old limit as behavior all said
the same thing: the peel presumes segments[1] is name text. It no
longer presumes it -- it asks _vocab.is_wholly_suffix, segment's own
suffix-comma predicate -- so all three say that instead.

AGENTS.md names the predicate and keeps the paragraph's density.
docs/release_log.rst EDITS the #312 entry rather than contradicting it
(2.1.0 is unreleased) and adds the #319 entry after it. docs/usage.rst
is rewritten at its own level of detail.

Three facts the rewrites are careful about, all measured rather than
assumed:

  * the peel is uniform across the three spellings; the PLACEMENT is
    not. 'PhD' -> title, 'V.' -> given, 'Ph. D.' -> suffix (as
    'さん, Ph. D.'). "One answer" is true of the peel alone, and each
    site says so.
  * lenient_comma_suffixes=False is NOT a blanket freeze. It restores
    the pre-#319 reading for the initial-shaped suffixes ('V.', 'V',
    'I') -- where the strict/lenient gap lives -- and not for
    'Ph. D.', whose merged 'phd' passes the strict test, so that run
    is declined and the peel fires under the knob too.
  * the scan set changed for EVERY wholly-suffix post-comma run, not
    only the #319 inputs. '田中さん, PhD' and '田中さん, 様.' reach
    さん now by declining the run where they used to reach it by
    stepping over the credential. Same answer, different traversal.

Then a sweep on that property rather than on phrasings: the peel was
instrumented to record which runs it scans, run over the whole case
table under five policies, and the flagged rows read against their
notes. Everything Task 3 fixed checks out, and the two other flagged
rows (ko_honorific_after_comma, ko_honorific_written_with_a_period)
describe classify's work, not the traversal, so they are unaffected.

The one stale thing the sweep did turn up is older than #319:
test_the_peel_crosses_a_family_comma_and_stops_there claimed both its
inputs pin the two-run slice. Measured by widening the slice to three,
only '김, 민준씨, 박씨' does -- 'Jr.' is a post-nominal by the strict
test as well, so a wider scan steps over it and peels correctly
anyway. The comment had borrowed the strict/lenient argument from the
test above it, where it belongs to 'V.'. Comment only; the assertions
are right and unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The #319 decline read "this run is wholly suffix" as "this run is not
name text", but is_wholly_suffix reaches period_joined_vocab, which
calls an interior-period token a suffix when ANY chunk is suffix
vocabulary -- and every honorific tail is a suffix WORD by the Lexicon
invariant. So a run's only claim to being suffix-shaped can be the
honorific glued to it, which is precisely what the peel exists to
remove: "이, J.씨" declined, segments[0] held no tail to peel instead,
and the given name went to suffix glued to its honorific.

Decline only where segments[0] holds a peel site of its own, so
declining can never cost the only site. "田中さん, V." keeps #319's
answer (さん is right there to peel), and the two-honorific
"김민준씨, J.씨" keeps it too -- both runs offer a site, so the
person's own 씨 is peeled rather than the junk one behind the comma.

The gate asks for that site with _peel_site, the peel's own scan-back
lifted out and shared, rather than approximating it: a cheaper test for
a token ENDING in a tail counts one that IS a tail entire, which the
scan skips as a site, so "선생님, J.씨" would decline and then find
nothing to cut. Measured over an 87,073-input sweep on an alphabet
built from the routes into is_wholly_suffix (period_joined_vocab,
splits_into_suffixes, the initial-shaped suffix words): 0 lost peels
against the pre-#319 unconditional scan under the default policy,
Policy(lenient_comma_suffixes=False) and the JA pack; the approximating
gate loses 418 and 410 on the first two.

Three case rows pin it, classified against 1.4.0 (worker_v1.py,
nameparser.__version__ 1.4.0): 1.4 peels none of the three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A mutation pass found the decline pinned exclusively by field-level
rows. Reverting the fix outright -- always segments[:2] under
FAMILY_COMMA -- failed six node ids, all of them case-table
parametrizations, and not one of test_script_segment.py's eight peel
tests, the file whose whole subject is which runs the peel scans. A
field-level pin passes for the wrong reason as soon as the traversal
changes, which is how three survivors got in.

  - Declining only runs of <=2 tokens survives the whole suite: both
    #319 rows have a 1- and a 2-token run.
  - The peel handing is_wholly_suffix a policy with
    extra_suffix_delimiters emptied survives, though the stage header
    promises the field reaches here.
  - Restricting the Ph./D. merge to position 0 survives, and it moves
    plain Latin: "John Smith, Jr. Ph. D." flips SUFFIX_COMMA ->
    FAMILY_COMMA, and "John Smith, MD, Jr. Ph. D." keeps every field
    while gaining a spurious comma-structure ambiguity, so even a
    field-level differential misses it.

Three stage tests state the traversal itself, in the peel section
alongside test_the_peel_crosses_a_family_comma_and_stops_there, which
was the standard: a wholly-suffix post-comma run is not scanned at
all; a THREE-token one is not either (the decline reads vocabulary,
not length); and a configured suffix delimiter reaches the peel
through both of its routes -- splits_into_suffixes for a no-whitespace
core inside one token ("PhD/MD"), plain core membership for a padded
one that tokenizes standalone ("Jr. - V.") -- each against the
bare-policy baseline that shows it is the delimiter doing the work.
Measured: the three fail under the wholesale revert, the last two
under the length gate, the delimiter one under the emptied policy.

The merge gets its own two, away from the peel: an is_wholly_suffix
assertion on ["Jr.", "Ph.", "D."] and the segment-stage structure of
"John Smith, Jr. Ph. D.". Two more assertions cover the routes into
is_wholly_suffix that until now died only in the v1 banks and the case
table -- period_joined_vocab and splits_into_suffixes -- so a break in
either is diagnosed where it happens.

Three case rows carry what belongs at field level, all classified
against 1.4.0 (worker_v1.py by script path, nameparser.__version__
reporting 1.4.0 from inside):

  - "김민준씨, V." and "王先生, V.", the decline in the other two
    scripts. Nothing in it reads a script -- it asks a vocabulary
    predicate about the post-comma run and the peel's scan-back about
    segments[0] -- but every witness the table had was written in
    kana. 1.4.0 gives first "V." / last 김민준씨 and 王先生, peeling
    neither: fix(#319) both.
  - "田中さん, Ph. D." under Policy(lenient_comma_suffixes=False).
    Two rows and a stage comment assert that the knob does not freeze
    the decline wholesale -- the merged "phd" satisfies
    is_suffix_strict, so this run is declined under either setting --
    and nothing held it. Identical fields to its default-policy twin,
    same fix(#319) against 1.4.0's first 田中さん / suffix "Ph. D.".

Four neighbouring notes counted the knob's exercises at two; there are
three. corpus_cjk.jsonl regenerates from the table and gains the two
new CJK texts; the harness stays clean at 748 names, 104 intentional
diffs, unexplained 0, both new inputs landing under the existing
fix(cjk-comma-compound) rule with no new entry in
expected_changes.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four reviewers found prose on this branch claiming mechanisms the code
does not have. Each claim below was re-measured before rewriting.

The load-bearing error: two case-table notes said the #320 initial veto
would reach is_wholly_suffix and abandon the peel on "田中さん, 様." the
way it once abandoned it on the scan-back. It cannot. Under the row's
default policy is_wholly_suffix selects is_suffix_lenient, whose
contract is suffix_words accepted unconditionally, bypassing the veto,
and _normalize("様.") is "様" -- a suffix word. Simulating the veto
peels さん there rather than stranding it; the described mechanism holds
only under Policy(lenient_comma_suffixes=False), which neither row sets.
Commit 4aff219's message carries the same claim and cannot be amended,
so the correction is recorded in the row.

The release log's #320 entry made the same mistake in behavior terms:
reverting #320 no longer strands さん in the comma form. Swapped its
example to the spaced "田中さん 様.", which still does, and said what
the comma form now depends on #320 for. 2.1.0 is unreleased, the same
standard already applied to the #312 entry.

The #319 entry described an unconditional decline; the decline has
taken a second condition since 838de67. It now says so, names
"이, J.씨" as what the condition protects and "김민준씨, J.씨" as where
the decline still stands, stops calling the listed honorific "様." a
credential, and scopes "every such input" to inputs whose run is
actually declined. Its "no name that peeled before stops peeling" was
verified rather than assumed: a 57k-input sweep against the pre-#319
tree over four policies finds 0 lost peels at HEAD and 68 at 60f1922,
the commit before the site condition.

Five more, each measured:
- the stage header stated exactly the plural-of-_is_post_nominal
  equation that _vocab's docstring and
  test_is_wholly_suffix_is_not_the_plural_of_is_post_nominal warn
  against; it now names the disagreement instead
- "田中さん, Jr./V." flips through splits_into_suffixes, not through a
  delimiter-core token: period_joined_vocab reads "Jr./V." as a title
  and "Jr./V." is not in cores. The bare-core route is "田中さん, /"
- the peeled remainder is 田中 and lands in family under all three
  credential spellings; what differs is where the CREDENTIAL lands
- the junk-tail hazard cited "Dr 김민준씨, Jr., 박씨", which is
  SUFFIX_COMMA with 박씨 in a third run and out of reach under either
  scope rule. "김민준씨, J.씨" is the input the guard really answers,
  and it agrees with the test comment that already got this right
- a cases.py note cited a passage 559e95c deleted

Plus two the flag reaches that the docs did not: customize.rst's
lenient_comma_suffixes row now records that the same test decides
whether the peel crosses a family comma, and _group.py notes that
is_wholly_suffix has two callers to keep its Ph./D. merge in sync with.
_segment.py's header said "every post-first segment" where only the
second decides, and "lenient" where the knob can make it strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six follow-ups from the review of the previous commit and of 838de67.
All prose; all the same species, a rule true in its context written as
though true everywhere. Each re-measured before rewriting.

"The two predicates disagree on exactly the initial-shaped suffix
words" was false, and false in the sentence written to prevent this
confusion -- the same sentence lists period_joined_vocab and the
delimiter routes as the run predicate's own, which is where the rest
of the disagreement comes from:

    V.        shaped=True   strict=False  wholly=True
    Msc.Ed.   shaped=False  strict=False  wholly=True
    J.씨      shaped=False  strict=False  wholly=True

Not curiosities: "田中さん, Msc.Ed." and "田中さん, J.씨" both go family
田中さん -> family 田中 with さん in suffix, so they are #319 fixes this
branch ships unmentioned. Corrected in all three places that carried
the wording -- the stage header, the peel's inline comment, and
AGENTS.md.

AGENTS.md and docs/usage.rst described the decline as unconditional;
both were last touched at 60f1922, one commit before 838de67 added the
site condition. Measured, the test is necessary and not sufficient:

    이, J.씨      wholly=True  site0=False  declined=False
    선생님, J.씨   wholly=True  site0=False  declined=False
    김민준씨, J.씨  wholly=True  site0=True   declined=True

The worked outcomes in usage.rst do not move -- a name with no site
before the comma has nothing to peel either way -- so it was the
mechanism that was wrong, not the examples. customize.rst's
lenient_comma_suffixes row, added by the previous commit, had the same
sole-decider framing and now says one of two.

"segment gives SUFFIX_COMMA whenever more than one word precedes the
comma" (in _peel_site's docstring and a cases.py note) omits the other
conjunct: "Dr 김민준, 지훈" has two words before the comma and is
FAMILY_COMMA. The conclusion survives, because the gate is asked only
where the second run is already suffix-shaped, so a FAMILY_COMMA that
reaches it failed on the word count and segments[0] holds at most one
token -- 0 violations over 2400 gate calls here, and the reviewer's
32,175. Both sites now state the implication with what the gate knows.

_peel_site's "circular at THIS call site alone (segment asks it of a
run nothing has peeled yet)" did not distinguish the call sites, since
segment asks just as early. The distinction that holds is how the
answer is spent: segment reads the run's shape and stops, the answer
BEING the structure, while the peel reads it and then decides whether
to strip the honorific that produced it.

And _segment.py's header said a segment beyond the second that is not
entirely suffix is flagged COMMA_STRUCTURE, where line 82 skips
empties -- "John Smith, MD,, Jr." reports nothing, "John Smith, MD,
Bart" does. The inline comment had the carve-out; only the header
omitted it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A finer mutation round on the gate itself found three survivors of the
previous commit, on axes every existing input holds constant. Two
inputs close all three, both stage tests, both in the peel section.

  - Keying the gate on "there is a second run" instead of on
    FAMILY_COMMA survives (84 field divergences). The table already
    states the intent it breaks -- ko_honorific_glued_given_suffix_
    comma_initial says a SUFFIX comma keeps the whole name in
    segments[0] -- but that row cannot see it: its segments[0] is
    "Dr 김민준씨", which holds a site, so the gate answers the same
    either way. The witness is its twin, two words before the comma
    with NO site among them and a junk tail beyond it:
    "John Smith, J.씨" peels 씨 off a stray post-nominal under the
    mutant and must not.
  - Reading segments[-1] rather than segments[1] survives, and so does
    requiring exactly two segments before declining (864 divergences)
    -- the direct sibling of the token-count gate closed in the last
    commit, on the segment-count axis instead. Every #319 row and all
    three new stage tests have exactly two segments, which is why it
    stayed open. "田中さん, V., 太郎" fails identically under both:
    the peel is silently lost, family 田中さん.

Measured, each introduced and watched to fail: the suffix-comma
witness gives ['John','Smith','J.','씨'] against ['John','Smith',
'J.씨'], and the three-segment witness ['田中さん','V.','太郎']
against ['田中','さん','V.','太郎'] under both of its mutants.

test_a_wholly_suffix_post_comma_run_is_not_scanned_at_all is renamed
to ..._run_after_a_family_comma_is_declined. Its body pins the
two-segment FAMILY_COMMA instance and all three survivors above passed
it, so the name asserted a universal the assertion did not hold -- the
same over-claiming this branch spent two commits correcting in prose,
this time in a test name. The two new tests hold the rest of the
universal; the name now says which instance it is.

Last commit's Latin Ph./D. pin stayed at the segment stage on the
grounds that a case row would need a new classification slug. That was
the wrong reason -- the table carries three ad-hoc slugs of the same
shape already -- so the row goes in:
suffix_comma_split_phd_after_another_suffix,
"John Smith, Jr. Ph. D.", fix(credential-pair-order). 1.4.0 gives
suffix "Ph. D., Jr." where 2.0 gives "Jr. Ph. D.": fix_phd extracted
the pair pre-parse and re-appended it, reordering the tail, where 2.0
renders it as written. It takes the V14 kill count from two node ids
to four.

That divergence turned out to be guarded nowhere. expected_changes.
toml deliberately files no rule for trailing "Ph. D." on the grounds
that the healing is parity and "if it ever starts diffing, the harness
must fail" -- but it does not: measured on a probe corpus, this input
comes out unexplained 0, absorbed by fix(comma-family), whose
name_regex is a bare comma and whose fields list contains suffix. The
comment there now scopes its parity claim to the two inputs it names
and records the hole. Not fixed in passing, and the note says why:
classify() takes the FIRST matching rule and the sort has only two
tiers, so a narrower rule could win only by file position -- making
file order load-bearing again, which the sort exists to prevent.
Giving classify() a real specificity order is a harness change, not a
rule change.

Gates: 2936 passed, 19 skipped, 11 xfailed; ruff and mypy clean;
sphinx html and doctest builds clean, README doctest clean; the
differential unchanged at 748 names, 104 intentional diffs,
unexplained 0 (corpus_cjk.jsonl regenerates identical -- the two new
witnesses are stage tests and the case row is Latin). All seven
mutants of this branch verified killed against the final tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73 derek73 self-assigned this Aug 3, 2026
@derek73 derek73 added the bug label Aug 3, 2026
@derek73 derek73 added this to the v2.1 milestone Aug 3, 2026
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.47%. Comparing base (373846b) to head (a1bb2ee).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #327   +/-   ##
=======================================
  Coverage   98.47%   98.47%           
=======================================
  Files          41       41           
  Lines        2811     2823   +12     
=======================================
+ Hits         2768     2780   +12     
  Misses         43       43           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The lone-token argument -- why segments[0] holds at most one token
where the gate asks, and why the word count alone does not say so --
was written out at full length twice, in _peel_site's docstring and in
the 선생님 row's note, counterexample and all.

Keep it at the definition site: someone changing the gate or the scan
lands there, and the bound is a precondition on the code they are
touching. The row keeps what it pins and why it is one token before
the comma, and points at the derivation instead of repeating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73
derek73 merged commit 7f4074d into master Aug 3, 2026
11 checks passed
@derek73
derek73 deleted the fix/wholly-suffix-predicate branch August 3, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Glued honorific: "田中さん, V." does not peel where "田中さん, PhD" does

1 participant